/**
 * @file Tilemap.cpp
 *
 * @date 01/02/2012
 * @author Tales 
 *
 */

#include "Tilemap.hpp"
#include <cstring>
#include "errors.hpp"
#include "Engine.hpp"
#include <algorithm>

using namespace std;

namespace wolf {

void Tilemap::_init(SDL_Surface *surface_, Uint8 tilesetColumns_,
		Uint8 tilesetRows_, Uint16 mapWidth_, Uint16 mapHeight_,
		Uint16 animationSlots_, Uint8 animationFrames_) {
	WOLF_ASSERT(surface_ !=NULL);
	surface = surface_;
	tilesetColumns = tilesetColumns_;
	tilesetRows = tilesetRows_;
	tileWidth = surface->w / tilesetColumns_;
	tileHeight = surface->h / tilesetRows_;
	mapWidth = mapWidth_;
	mapHeight = mapHeight_;
	animationSlots = animationSlots_;
	animationFrames = animationFrames_;
	tiles = new Sint16[mapHeight_ * mapWidth_];
	animation = new Uint16[animationSlots_ * animationFrames_];
	animationSpeed = 1;
	x = y = 0;
	offsetX = offsetY = 0;
	viewWidth = tileWidth * mapWidth_;
	viewHeight = tileHeight * mapHeight_;
	verticalLoop = horizontalLoop = false;
}

Tilemap::Tilemap(SDL_Surface *surface_, Uint8 tileWidth_, Uint8 tileHeight_,
		Uint16 mapWidth_, Uint16 mapHeight_, Uint16 animationSlots_,
		Uint8 animationFrames_, bool own, int depth) :
		Layer(depth) {
	_init(surface_, tileWidth_, tileHeight_, mapWidth_, mapHeight_,
			animationSlots_, animationFrames_);
	if (!own)
		++surface->refcount;
	memset(tiles, 0, mapWidth_ * mapHeight_ * sizeof(tiles[0]));
}

Tilemap::Tilemap(SDL_Surface *surface_, Uint8 tileWidth_, Uint8 tileHeight_,
		Uint16 mapWidth_, Uint16 mapHeight_, const Sint16 *tiles_,
		Uint16 animationSlots_, Uint8 animationFrames_, bool own, int depth) :
		Layer(depth) {
	_init(surface_, tileWidth_, tileHeight_, mapWidth_, mapHeight_,
			animationSlots_, animationFrames_);
	if (!own)
		++surface->refcount;
	memcpy(tiles, tiles_, mapWidth_ * mapHeight_ * sizeof(tiles[0]));
}

Tilemap::~Tilemap() {
	SDL_FreeSurface(this->surface);
	delete[] tiles;
	delete[] animation;
}

void Tilemap::drawTile(Sint16 tile, Sint16 column, int iniX, Sint16 row,
		int iniY, SDL_Surface *& dst) {
	if (tile < 0) {
		Uint16 animationId = ~tile;
		Uint16 index = (Uint16) ((Engine::getInstance().getCurrentFrame()
				* animationSpeed)) % animationFrames;
		tile = animation[animationId * animationFrames + index];
	}
	if (tile > 0) {
		--tile;
		SDL_Rect srcRect = { tile % tilesetColumns * tileWidth, tile
				/ tilesetColumns * tileHeight, tileWidth, tileHeight };
		SDL_Rect dstRect =
				{ column * tileWidth + iniX, row * tileHeight + iniY };
		SDL_BlitSurface(surface, &srcRect, dst, &dstRect);
	}
}
void Tilemap::draw(SDL_Surface *dst) {
	WOLF_ASSERT(dst!= NULL);
	// position on screen
	int iniX = x - offsetX;
	int iniY = y - offsetY;
	//Horizontal bondaries:
	Sint16 firstColumn;
	Sint16 columnsToDraw;
	if (horizontalLoop) {
		//Left boundary
		firstColumn = (offsetX / tileWidth - 1);
		//Right boundary
		columnsToDraw = mapWidth + firstColumn + 2;
	} else {
		//Left boundary
		firstColumn = max(offsetX / tileWidth, 0);
		//Right boundary
		Uint16 lastPixelX = offsetX + viewWidth;
		columnsToDraw = min(lastPixelX / tileWidth + 1, (int) (mapWidth));
	}
	Sint16 firstRow;
	Sint16 rowsToDraw;
	if (verticalLoop) {
		//Up boundary
		firstRow = (offsetY / tileHeight - 1);
		//Down boundary
		rowsToDraw = mapHeight + firstRow + 2;
	} else {
		//Up boundary
		firstRow = max(offsetY / tileHeight, 0);
		//Down boundary
		Uint16 lastPixelY = offsetY + viewHeight;
		rowsToDraw = min(lastPixelY / tileHeight + 1, (int) (mapHeight));
	}

	//Backup the current clip rect
	SDL_Rect oldClipRect = dst->clip_rect;
	//Changing the clip rect to the draw area. This ensures the mapping will not exceed the view area
	SDL_Rect mapClipRect = { x, y, viewWidth, viewHeight };
	SDL_SetClipRect(dst, &mapClipRect);

	//The draw routine TODO: Optimize it. Seriously it was did by trying and failing and a more careful study could do it more elegant.
	for (Sint16 row = firstRow; row < rowsToDraw; ++row) {
		for (Sint16 column = firstColumn; column < columnsToDraw; ++column) {
			Uint16 rowFix = ((row >= 0) ? (row) : (mapHeight + row % mapHeight))
					% mapHeight;
			Uint16 columnFix = (
					(column >= 0) ? (column) : (mapWidth + column % mapWidth))
					% mapWidth;
			Uint16 tilePos = rowFix * mapWidth + columnFix;
			Sint16 tile = tiles[tilePos];
			drawTile(tile, column, iniX, row, iniY, dst);
		}
	}
	// Restoring the backup.
	SDL_SetClipRect(dst, &oldClipRect);
}

Sint16 Tilemap::tileAt(size_t x, size_t y) const {
	WOLF_ASSERT(x < mapWidth && y < mapHeight);
	return tiles[y * mapWidth + x];
}

Sint16 & Tilemap::tileAt(size_t x, size_t y) {
	WOLF_ASSERT(x < mapWidth && y < mapHeight);
	return tiles[y * mapWidth + x];
}

Uint16 Tilemap::getMapHeight() const {
	return mapHeight;
}

Uint16 Tilemap::getMapWidth() const {
	return mapWidth;
}

SDL_Surface *Tilemap::getSurface() const {
	return surface;
}

Uint8 Tilemap::getTilesetColumns() const {
	return tilesetColumns;
}

bool Tilemap::isHorizontalLoop() const {
	return horizontalLoop;
}

bool Tilemap::isVerticalLoop() const {
	return verticalLoop;
}

void Tilemap::setHorizontalLoop(bool horizontalLoop) {
	this->horizontalLoop = horizontalLoop;
}

void Tilemap::setSurface(SDL_Surface *surface, bool own) {
	if (this->surface == surface)
		return;
	SDL_FreeSurface(this->surface);
	this->surface = surface;
	if (!own)
		++this->surface->refcount;
	tileWidth = surface->w / tilesetColumns;
	tileHeight = surface->h / tilesetRows;
}

void Tilemap::setVerticalLoop(bool verticalLoop) {
	this->verticalLoop = verticalLoop;
}

Uint8 Tilemap::getTileHeight() const {
	return tileHeight;
}

Uint8 Tilemap::getTileWidth() const {
	return tileWidth;
}

Uint8 Tilemap::getTilesetRows() const {
	return tilesetRows;
}

Uint8 Tilemap::getAnimationFrames() const {
	return animationFrames;
}

Uint16 Tilemap::getAnimationSlots() const {
	return animationSlots;
}

float Tilemap::getAnimationSpeed() const {
	return animationSpeed;
}

const Sint16 *Tilemap::getTiles() const {
	return tiles;
}

const Uint16 *Tilemap::getAnimation() const {
	return animation;
}

const Uint16 *Tilemap::getAnimation(Sint16 slot) const {
	WOLF_ASSERT(slot < 0 && slot >= -animationSlots);
	return animation + (~slot * animationFrames);
}

Uint16 Tilemap::getAnimation(Sint16 slot, Uint8 frame) const {
	WOLF_ASSERT(slot < 0 && slot >= -animationSlots);
	WOLF_ASSERT(frame < -animationFrames);
	return animation[~slot * animationFrames + frame];
}

void Tilemap::setAnimation(Sint16 slot, const Uint16 *frames) {
	WOLF_ASSERT(slot < 0 && slot >= -animationSlots);
	memcpy(this->animation + (~slot * animationFrames), frames,
			sizeof(Uint16) * animationFrames);
}

void Tilemap::setAnimation(Sint16 slot, Uint8 frame, Uint16 value) {
	WOLF_ASSERT(slot < 0 && slot >= -animationSlots);
	WOLF_ASSERT(frame < -animationFrames);
	animation[~slot * animationFrames + frame] = value;
}

void Tilemap::setAnimation(const Uint16 *animation) {
	memcpy(this->animation, animation,
			sizeof(Uint16) * animationSlots * animationFrames);
}

void Tilemap::setAnimationSpeed(float animationVelocity) {
	this->animationSpeed = animationVelocity;
}

void Tilemap::setTiles(const Sint16 *tiles) {
	memcpy(this->tiles, tiles, sizeof(Sint16) * mapHeight * mapWidth);
}

Sint16 Tilemap::getOffsetX() const {
	return offsetX;
}

Sint16 Tilemap::getOffsetY() const {
	return offsetY;
}

Uint16 Tilemap::getViewHeight() const {
	return viewHeight;
}

Uint16 Tilemap::getViewWidth() const {
	return viewWidth;
}

int Tilemap::getX() const {
	return x;
}

int Tilemap::getY() const {
	return y;
}

void Tilemap::setOffsetX(Sint16 offsetX) {
	this->offsetX = offsetX;
}

void Tilemap::setOffsetY(Sint16 offsetY) {
	this->offsetY = offsetY;
}

void Tilemap::setViewHeight(Uint16 viewHeight) {
	this->viewHeight = viewHeight;
}

void Tilemap::setViewWidth(Uint16 viewWidth) {
	this->viewWidth = viewWidth;
}

void Tilemap::setX(int x) {
	this->x = x;
}

void Tilemap::setY(int y) {
	this->y = y;
}

} /* namespace wolf */
